namespace Microsoft.Robotics.Core.Algorithms
{
    using System;
    using Microsoft.Robotics.Numerics;

    /// <summary>
    /// Precision/Recall calculator. Allows computing basic Precision, Recall, Accuracy, Specificity values
    /// based on added measurements. Values based on Sampler, which also allows getting standard deviations
    /// of those metrics for the sampling session
    /// </summary>
    public sealed class PRCalculator
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="PRCalculator" /> class.
        /// </summary>
        public PRCalculator()
        {
            this.TruePositives = new Sampler();
            this.FalsePositives = new Sampler();
            this.TrueNegatives = new Sampler();
            this.FalseNegatives = new Sampler();
            this.RollingPrecision = new Sampler();
            this.RollingRecall = new Sampler();
        }

        /// <summary>
        /// Gets true positives
        /// </summary>
        public Sampler TruePositives { get; private set; }

        /// <summary>
        /// Gets false positives
        /// </summary>
        public Sampler FalsePositives { get; private set; }

        /// <summary>
        /// Gets total true positives 
        /// </summary>
        public Sampler TrueNegatives { get; private set; }

        /// <summary>
        /// Gets false negatives
        /// </summary>
        public Sampler FalseNegatives { get; private set; }

        /// <summary>
        /// Gets rolling precision
        /// </summary>
        public Sampler RollingPrecision { get; private set; }

        /// <summary>
        /// Gets rolling recall
        /// </summary>
        public Sampler RollingRecall { get; private set; }        

        /// <summary>
        /// Gets precision (fraction of retrieved instances that are relevant) based on sample totals
        /// </summary>
        public double TotalPrecision
        {
            get
            {
                return (this.TruePositives.Total + this.FalsePositives.Total) > 0 ?
                    this.TruePositives.Total / (this.TruePositives.Total + this.FalsePositives.Total) : 0;
            }
        }

        /// <summary>
        /// Gets recall (fraction of relevant instances that are retrieved) based on sample totals
        /// </summary>
        public double TotalRecall
        {
            get
            {
                return (this.TruePositives.Total + this.FalseNegatives.Total) > 0 ?
                    this.TruePositives.Total / (this.TruePositives.Total + this.FalseNegatives.Total) : 0;
            }
        }

        /// <summary>
        /// Gets total specificity (the proportion of negatives which are correctly identified as such)
        /// </summary>
        public double TotalSpecificity
        {
            get
            {
                return (this.TrueNegatives.Total + this.FalsePositives.Total) > 0 ?
                    this.TrueNegatives.Total / (this.TrueNegatives.Total + this.FalsePositives.Total) : 0;
            }
        }

        /// <summary>
        /// Gets total accuracy (proportion of true results (both true positives and true negatives) in the population)
        /// </summary>
        public double TotalAccuracy
        {
            get
            {
                double denom = (this.TruePositives.Total + this.TrueNegatives.Total + this.FalsePositives.Total + this.FalseNegatives.Total);

                return denom > 0 ? (this.TruePositives.Total + this.TrueNegatives.Total) / denom : 0;
            }
        }

        /// <summary>
        /// Gets total F-score, also known as F-measure for any beta. Search the web for F-score for references.
        /// </summary>
        /// <param name="beta">Beta of F-score. Commonly used F measures are the F2 measure (beta = 2), which weights recall higher than precision, 
        /// the F0.5 (beta = 0.5) measure, which puts more emphasis on precision than recall, and balanced (beta=1).</param>
        /// <returns>F-score of measured samples</returns>
        public double TotalFScore(double beta)
        {
            return (1 + beta * beta) * (this.TotalPrecision * this.TotalRecall) / (beta * beta * this.TotalPrecision + this.TotalRecall);
        }

        /// <summary>
        /// Gets total N-score defined as N(α,γ)=Sin(π/2 P〗^α)*Sin(〖π/2 (R-ς)〗^γ)]. This measure behaves better than F-measure in cases where
        /// P is critical, and R can be given some slack. N-score can take values exceeding original values of P or R, but will never exceed 1
        /// π/2 is a normalization factor
        /// N-score is developed in-house (baker 2013) as a more relevant scoring system for floor extractor quality.
        /// </summary>
        /// <param name="alpha">Boosting factor for P. Values higher than 1 penalize imperfect P. Values lower than 1 have the opposite effect</param>
        /// <param name="gamma">Boosting factor for R. Values higher than 1 penalize imperfect R. Values lower than 1 have the opposite effect</param>
        /// <returns>N-score of measured samples</returns>
        public double NScore(double alpha, double gamma)
        {
            double recallWithStdDev = (this.TotalRecall - this.RollingRecall.StdDev);
            recallWithStdDev = recallWithStdDev > 0 ? recallWithStdDev : 0;

            double halfPi = Math.PI / 2;

            return Math.Sin(halfPi * Math.Pow(this.TotalPrecision, alpha)) * Math.Sin(halfPi * Math.Pow(recallWithStdDev, gamma));
        }

        /// <summary>
        /// Adds new measurement to the sample
        /// </summary>
        /// <param name="truePositives">Measured true positive count</param>
        /// <param name="falsePositives">Measured false positive count</param>
        /// <param name="trueNegatives">Measured true negative count</param>
        /// <param name="falseNegatives">Measured false negative count</param>
        public void AddMeasurement(int truePositives, int falsePositives, int trueNegatives, int falseNegatives)
        {
            this.TruePositives.Add(truePositives);
            this.FalsePositives.Add(falsePositives);
            this.TrueNegatives.Add(trueNegatives);
            this.FalseNegatives.Add(falseNegatives);

            if ((truePositives + falsePositives) > 0)
            {
                this.RollingPrecision.Add((double)truePositives / (truePositives + falsePositives));
            }

            if ((truePositives + falseNegatives) > 0)
            {
                this.RollingRecall.Add((double)truePositives / (truePositives + falseNegatives));
            }
        }
    }
}
